CircleCIでOrbsを利用して並列処理を行う

この記事は[CircleCI Advent Calendar 2018](https://qiita.com/advent-calendar/2018/circleci "CircleCI Advent Calendar 2018 - Qiita") 3日目の記事です。

前回の記事は @yasuhiroki さんの「[CircleCIのOnly build pull requestsをoffにしてもプルリクエストを作ったらJobを実行したい]( https://qiita.com/yasuhiroki/items/bddb3f51bf3439652c0c "CircleCIのOnly build pull requestsをoffにしてもプルリクエストを作ったらJobを実行したい")」でした


##はじめに

この記事ではCircleCIの並列処理についてAnsibleを使って解説します。この記事での検証結果は [![CircleCI](https://circleci.com/gh/TomonoriMatsumura/ansible_joke-programs_cowsay.svg?style=svg)](https://circleci.com/gh/TomonoriMatsumura/ansible_joke-programs_cowsay) で確認できます。

###この記事で解説する事

* CircleCIの並列処理の基本的なやり方
* CI/CDの処理時間を短縮する方法


###この記事で解説しない事

* Ansibleの使い方
* CircleCIの使い方
* Dockerの使い方
* Orbsの解説、導入方法

###この記事で言いたい事

* Orbsを利用してパイプラインの再利用性を高めましょう
* Orbsに定義出来ない処理、もしくはOrbsで定義すると再利用が難しくなる処理は「.circleci/config.yml」へ書きましょう
* CircleCIで処理する必要がないものはあらかじめDockerイメージ化しレジストリへ登録しておきましょう（スナップショットの作成）
* CircleCIで利用する独自のDockerイメージはcron等を用いて頻繁にアップデートしましょう（スナップショットのリフレッシュ）

###Ansibleとは

>Ansible（アンシブル）は、レッドハットが開発するオープンソースの構成管理ツールである。サーバを立ち上げる際、あらかじめ用意した設定ファイルに従って、ソフトウェアのインストールや設定を自動的に実行する事が出来る[1]。特に大規模なコンピュータ・クラスターを構築する時に、時間の短縮やミスの削減に有用である。構成管理に加え、オーケストレーションやソフトウェアデプロイメントの機能を持つ　

[Ansible (ソフトウェア) - Wikipedia](https://ja.wikipedia.org/wiki/Ansible_(ソフトウェア) "Ansible (ソフトウェア) - Wikipedia") より引用

###Ansibleについての注意点

Ansibleは通常OSやネットワーク機器に対して利用されます。この記事みたいにDockerで利用する場合 **Ansibleの性能を100％引き出す事はできません** 。

この記事ではあくまでCircleCIで並列処理を説明するための事例として取り上げました。

実際にAnsibleを利用する際はDockerではなくOS環境下で実行してください。


##シナリオ

###目標

**複数の異なるディストリビューション** に **複数の異なるバージョンのAnsible** を利用しプログラムをインストールする処理のCIを行う。

CI/CDの速度を速めるためにOSイメージの「 **スナップショット** 」を利用し「 **並列で処理** 」をする。

###処理の流れ
1. Playbook取得
2. 静的解析（lintを実行する）
3. Playbook実行

###想定するディストリビューション
* CentOS7
* Ubuntu18

###利用するAnsibleのバージョン
* 2.7系（aptもしくはyumコマンドでインストールされるAnsible）
* 2.7系（pipコマンドでインストールされるAnsible）
* 2.8系

全部で 「2系統のディストリビューション」 x 「3系統のAnsible」 の6系統の並列処理を行います。

![circleci-2.png](https://qiita-image-store.s3.amazonaws.com/0/79373/23d66ec6-6db3-c380-7a07-3b9cb18cee66.png)

###インストールするプログラム
* [cowsay - Wikipedia](https://ja.wikipedia.org/wiki/Cowsay "cowsay - Wikipedia")

![cowsay.gif](https://qiita-image-store.s3.amazonaws.com/0/79373/dedaf060-f42f-4257-d99b-8980797b3cc3.gif)

###使用するAnsible Playbook

* [TomonoriMatsumura/ansible_joke-programs_cowsay | GitHub](https://github.com/TomonoriMatsumura/ansible_joke-programs_cowsay "TomonoriMatsumura/ansible_joke-programs_cowsay")

##準備作業としてDockerイメージをレジストリサービスへ登録する

CI/CDの速度を上げるためOSイメージのスナップショットを作成しておきます。

###1.Dockerfileを作成する

あらかじめ「CentOS7」と「Ubuntu18」に 

- Ansible
- Ansible-lint
- Git

をインストールしたDockerイメージを作成しレジストリへ登録します。



####CentOS7へAnsible、Ansible-lint、GitをインストールするDockerfileの例

<details>
<div>

```dockerfile:Dockerfile
FROM centos:latest

RUN set -x && \
    rm -f /etc/rpm/macros.image-language-conf && \
    sed -i '/^override_install_langs=/d' /etc/yum.conf && \
    yum -y reinstall glibc-common && \
    yum clean all && \
    yum update -y && \
    yum install -y epel-release git && \
    yum -y install ansible && \
    yum -y install expect python-devel python-pip && \
    pip install --upgrade pip && pip install --upgrade setuptools && \
    pip install ansible-lint

ENV LANG="ja_JP.UTF-8" \
    LANGUAGE="ja_JP:ja" \
    LC_ALL="ja_JP.UTF-8"

```
</div>
</details>

####Ubuntu18へAnsible、Ansible-lint、GitをインストールするDockerfileの例

<details>
<div>

```dockerfile:Dockefile
FROM ubuntu:latest

RUN apt update -y && \
    apt install -y language-pack-ja-base language-pack-ja && \
    apt install -y software-properties-common && \
    apt-add-repository -y ppa:ansible/ansible && \
    apt update -y && \
    apt install -y ansible && \
    apt install -y curl python-dev git && \
    curl "https://bootstrap.pypa.io/get-pip.py" -o "/tmp/get-pip.py" && \
    python /tmp/get-pip.py && \
    pip install --upgrade pip && pip install --upgrade setuptools && \
    pip install ansible-lint

ENV LANG="ja_JP.UTF-8" \
    LANGUAGE="ja_JP:ja" \
    LC_ALL="ja_JP.UTF-8"

```

</div>
</details>



###2. Docker HubへDockerイメージを登録する

この記事ではレジストリに [Docker Hub](https://hub.docker.com "Docker Hub") を利用します。

[Docker Hub](https://hub.docker.com "Docker Hub") は [GitHubへDockerfileをpushすると自動でDockerイメージをbuildするサービス](https://qiita.com/takeshi_hirosue/items/9cfd22c2263866cece99 "Docker HubでDockerfileを公開する(AUTOMATED BUILD) - Qiita") を提供しています。

そのため

- 記事等で利用する
- OSSの活動で利用する

など「作成したDockerイメージの内容を開示したい」場合は有用なツールになります。

###利用するDockerイメージ

* [tomonorimatsumura/centos7-ansible - Docker Hub](https://hub.docker.com/r/tomonorimatsumura/centos7-ansible/ "tomonorimatsumura/centos7-ansible - Docker Hub")
* [tomonorimatsumura/ubuntu18-ansible - Docker Hub](https://hub.docker.com/r/tomonorimatsumura/ubuntu18-ansible/ "tomonorimatsumura/ubuntu18-ansible - Docker Hub")


##Ansible Playbookを作成する

 [こちら](https://github.com/TomonoriMatsumura/ansible_joke-programs_cowsay "TomonoriMatsumura/ansible_joke-programs_cowsay | GitHub")のPlaybookを使用します。



Ansible Playbookでは以下の処理を定義しています。

1. Playbookを実行するDockerコンテナのディストリビューション情報を取得する
2. ディストリビューションによって処理を選択し実行する


```yaml:install.yml
- hosts: all
  tasks:
    - name: Install cowsay with yum
      include_role:
        name: centos7
      when: ansible_distribution == "CentOS"

    - name: Install cowsay with apt
      include_role:
        name: ubuntu18
      when: ansible_distribution == "Ubuntu"
```

```yaml:roles/centos7/tasks/main.yml
- name: Install cowsay with yum
  become: yes
  yum:
    name: cowsay
    state: present
```

```yaml:roles/ubuntu18/tasks/main.yml
- name: Install cowsay with apt
  become: yes
  apt:
    name: cowsay
    state: present

- name: Copy cowsay execution file to '/usr/local/bin/'
  become: yes
  copy:
    src: /usr/games/cowsay
    dest: /usr/local/bin/cowsay
    owner: root
    group: root
    mode: 0755
```

##orbを作成する

この記事ではCircleCI 2.1から本格的に利用できる [Orbs](https://circleci.com/docs/2.0/creating-orbs/ "Creating Orbs - CircleCI") を使います。


###orbでの処理について


```yaml
version: 2.1

commands:
  show-os-version:
    description: "Show OS version information"
    steps:
      - run:
          name: Show OS version
          command: |
            if [ -e /etc/os-release ]; then
              cat /etc/os-release
            elif [ -e /etc/redhat-release ]; then
              cat /etc/redhat-release
            else
              echo 'Invalid Image' 1>&2
              exit 1
            fi

  ansible-lint:
    description: "Execute ansible-lint command"
    steps:
      - run:
          name: Show ansible-lint version
          command: ansible-lint --version
      - run:
          name: Retrieve Ansible lint rules file
          command: |
            cd /tmp
            git clone https://github.com/TomonoriMatsumura/ansible_joke-programs_lint-rules
      - run:
          name: Execute ansible-lint
          command: |
            cd $CIRCLE_WORKING_DIRECTORY
            find . -type f -name '*.yml' | xargs ansible-lint -c /tmp/ansible_joke-programs_lint-rules/.ansible-lint-rules

  ansible-playbook:
    description: "Execute ansible-playbook command"
    steps:
      - run:
          name: Show Ansible version
          command: ansible --version
      - run:
          name: Execute Ansible Playbook
          command: |
            cd $CIRCLE_WORKING_DIRECTORY
            ansible-playbook -i localhost install.yml
      - run:
          name: Execute Ansible Playbook again, checking to make sure it's idempotent.
          command: |
            cd $CIRCLE_WORKING_DIRECTORY
            ansible-playbook -i localhost install.yml

executors:
  centos7-ansible-yum:
    docker:
      - image: tomonorimatsumura/centos7-ansible:yum
  centos7-ansible-stable:
    docker:
      - image: tomonorimatsumura/centos7-ansible:stable
  centos7-ansible-devel:
    docker:
      - image: tomonorimatsumura/centos7-ansible:devel
  ubuntu18-ansible-apt:
    docker:
      - image: tomonorimatsumura/ubuntu18-ansible:apt
  ubuntu18-ansible-stable:
    docker:
      - image: tomonorimatsumura/ubuntu18-ansible:stable
  ubuntu18-ansible-devel:
    docker:
      - image: tomonorimatsumura/ubuntu18-ansible:devel
```

orbにて以下の処理を定義します。

1. 利用するDockerイメージのディストリビューション情報を表示する
2. Ansible-lintを使いPlaybookのシンタックスをチェックする
3. Playbookを実行する

これはcommandsセクションの 

1. show-os-version key
2. ansible-lint key
3. ansible-playbook key

がそれぞれ対応します。

またexecutorセクションでDockerイメージを定義しています。


##.circleci/config.yml を作成する

```yaml:.circleci/config.yml
show-os-version-steps: &show-os-version-steps
  steps:
    - ansible/show-os-version

lint-steps: &lint-steps
  steps:
    - checkout
    - ansible/ansible-lint

playbook-steps: &playbook-steps
  steps:
    - checkout
    - ansible/ansible-playbook
    - run:
        name: Execute cowsay
        command: |
          export TERM=xterm
          cowsay "hello world!"

version: 2.1

orbs:
  ansible: orbss/ansible@volatile

jobs:
  Ce7-yum_os-version:
    executor:
      name: ansible/centos7-ansible-yum
    <<: *show-os-version-steps
  Ce7-yum_lint:
    working_directory: /tmp/ansible
    executor:
      name: ansible/centos7-ansible-yum
    <<: *lint-steps
  Ce7-yum_playbook:
    working_directory: /tmp/ansible
    executor:
      name: ansible/centos7-ansible-yum
    <<: *playbook-steps

  Ce7-stable_os-version:
    executor:
      name: ansible/centos7-ansible-stable
    <<: *show-os-version-steps
  Ce7-stable_lint:
    working_directory: /tmp/ansible
    executor:
      name: ansible/centos7-ansible-stable
    <<: *lint-steps
  Ce7-stable_playbook:
    working_directory: /tmp/ansible
    executor:
      name: ansible/centos7-ansible-stable
    <<: *playbook-steps

  Ce7-devel_os-version:
    executor:
      name: ansible/centos7-ansible-devel
    <<: *show-os-version-steps
  Ce7-devel_lint:
    working_directory: /tmp/ansible
    executor:
      name: ansible/centos7-ansible-devel
    <<: *lint-steps
  Ce7-devel_playbook:
    working_directory: /tmp/ansible
    executor:
      name: ansible/centos7-ansible-devel
    <<: *playbook-steps

  Ub18-apt_os-version:
    executor:
      name: ansible/ubuntu18-ansible-apt
    <<: *show-os-version-steps
  Ub18-apt_lint:
    working_directory: /tmp/ansible
    executor:
      name: ansible/ubuntu18-ansible-apt
    <<: *lint-steps
  Ub18-apt_playbook:
    working_directory: /tmp/ansible
    executor:
      name: ansible/ubuntu18-ansible-apt
    <<: *playbook-steps

  Ub18-stable_os-version:
    executor:
      name: ansible/ubuntu18-ansible-stable
    <<: *show-os-version-steps
  Ub18-stable_lint:
    working_directory: /tmp/ansible
    executor:
      name: ansible/ubuntu18-ansible-stable
    <<: *lint-steps
  Ub18-stable_playbook:
    working_directory: /tmp/ansible
    executor:
      name: ansible/ubuntu18-ansible-stable
    <<: *playbook-steps

  Ub18-devel_os-version:
    executor:
      name: ansible/ubuntu18-ansible-devel
    <<: *show-os-version-steps
  Ub18-devel_lint:
    working_directory: /tmp/ansible
    executor:
      name: ansible/ubuntu18-ansible-devel
    <<: *lint-steps
  Ub18-devel_playbook:
    working_directory: /tmp/ansible
    executor:
      name: ansible/ubuntu18-ansible-devel
    <<: *playbook-steps

workflows:
  version: 2
  ansible-playbook:
    jobs:
      - Ce7-yum_os-version
      - Ce7-yum_lint:
          requires:
            - Ce7-yum_os-version
      - Ce7-yum_playbook:
          requires:
            - Ce7-yum_lint


      - Ce7-stable_os-version
      - Ce7-stable_lint:
          requires:
            - Ce7-stable_os-version
      - Ce7-stable_playbook:
          requires:
            - Ce7-stable_lint


      - Ce7-devel_os-version
      - Ce7-devel_lint:
          requires:
            - Ce7-devel_os-version
      - Ce7-devel_playbook:
          requires:
            - Ce7-devel_lint


      - Ub18-apt_os-version
      - Ub18-apt_lint:
          requires:
            - Ub18-apt_os-version
      - Ub18-apt_playbook:
          requires:
            - Ub18-apt_lint


      - Ub18-stable_os-version
      - Ub18-stable_lint:
          requires:
            - Ub18-stable_os-version
      - Ub18-stable_playbook:
          requires:
            - Ub18-stable_lint


      - Ub18-devel_os-version
      - Ub18-devel_lint:
          requires:
            - Ub18-devel_os-version
      - Ub18-devel_playbook:
          requires:
            - Ub18-devel_lint
```

###Anchors & Aliasesについて

|anchorの設定|aliasの配置|
|---|---|
|show-os-version-steps: &show-os-version-steps|<<: *show-os-version-steps|
|lint-steps: &lint-steps|<<: *lint-steps|
|playbook-steps: &playbook-steps|<<: *playbook-steps|

表にある記述がある箇所は 「.circleci/config.yml」 で利用できるAnchors & Aliasesの機能です。anchorを定義した後aliasを指定した箇所へ配置できます。

詳しくは[こちら](https://circleci.com/blog/circleci-hacks-reuse-yaml-in-your-circleci-config-with-yaml/ "YAML - config.yml | CircleCI")を参考にして下さい。


###workflowsでJobを並列実行する

workflowsセクションではそれぞれのJobを並列実行するように処理を書いています。CircleCIでJobを実行する順序を定義する詳しい方法は[こちら](https://circleci.com/docs/2.0/workflows/ "Using Workflows to Schedule Jobs - CircleCI")を参考にして下さい。

###利用するorbのバージョンについて

Orbsでは「circleci/config.yml」で参照するorbのバージョンを指定できます。ですがこの記事に関しては常に最新版を取得する **volatile** を使用しています。

実際にOrbsを利用する際には最新バージョンではなく特定のバージョン番号を指定した方が良い場合もあります。バージョンの指定の記述は適宜使い分けると良いでしょう。


##まとめ

CircleCIでCIを並列実行する方法を紹介しました。この方法を利用すれば、例えばバージョン違いの Ruby + Rails や PHP + Laravel を利用し並列処理で検証を行う、と言うような事も可能になります。

フレームワークや言語のバージョンの互換性を考慮した開発が可能になるかもしれません。


###CircleCIの並列処理のポイント

* Orbsを利用してパイプラインの再利用性を高める
* Orbsに定義出来ない処理、もしくはOrbsで定義すると再利用が難しくなる処理は「.circleci/config.yml」へ書く
* CircleCIで処理する必要がないものはあらかじめDockerイメージ化しレジストリへ登録する（スナップショットの作成）
* CircleCIで利用する独自のDockerイメージはcron等を用いて頻繁にアップデートする（スナップショットのリフレッシュ）

この辺りを意識すると良いと思います。特にコンテナを含む **OSイメージのスナップショット** は並列処理に次いでCI/CDのスピードに大きな影響を与えます。スナップショットは意識的に採用すべきでしょう。


##今後の課題

###並列処理のレポートの問題

今回使用した　「.circleci/config.yml」 ですが問題がひとつあります。それは並列処理だと実行するJobの順番が指定できないと言うことです。

####理想的なパイプライン

![circleci-2.png](https://qiita-image-store.s3.amazonaws.com/0/79373/23d66ec6-6db3-c380-7a07-3b9cb18cee66.png)

####実際のパイプライン
<img width="843" alt="スクリーンショット 2018-11-29 17.37.29のコピー.png" src="https://qiita-image-store.s3.amazonaws.com/0/79373/aec9c2b7-8e6f-be3e-e18b-f862831e4006.png">

パイプラインをリニアに表示したいのですがCircleCIは処理を実行した順にJobを表示するみたい?で意図したものと異なったものが出力されます。

この挙動を改善できると良いのですが今のところ方法が分かりません。



##参考リンク

###Ansible

* [Ansible (ソフトウェア) - Wikipedia](https://ja.wikipedia.org/wiki/Ansible_(ソフトウェア) "Ansible (ソフトウェア) - Wikipedia")
* [Ansible is Simple IT Automation](https://www.ansible.com "Ansible is Simple IT Automation")
* [TomonoriMatsumura/ansible_joke-programs_cowsay | GitHub](https://github.com/TomonoriMatsumura/ansible_joke-programs_cowsay "TomonoriMatsumura/ansible_joke-programs_cowsay")

###CircleCI

####Orbsについて
* [Creating Orbs - CircleCI](https://circleci.com/docs/2.0/creating-orbs/ "Creating Orbs - CircleCI")

####Anchor & Aliasesについて
* [YAML - config.yml | CircleCI](https://circleci.com/blog/circleci-hacks-reuse-yaml-in-your-circleci-config-with-yaml/ "YAML - config.yml | CircleCI")

####並列処理について
* [Using Workflows to Schedule Jobs - CircleCI](https://circleci.com/docs/2.0/workflows/ "Using Workflows to Schedule Jobs - CircleCI")

####記事の検証
* [この記事のレポート](https://circleci.com/gh/TomonoriMatsumura/ansible_joke-programs_cowsay "CircleCI")

###Cowsay

* [cowsay - Wikipedia](https://ja.wikipedia.org/wiki/Cowsay "cowsay - Wikipedia")

###Docker Hub

* [Docker Hub](https://hub.docker.com "Docker Hub")
* [Docker HubでDockerfileを公開する AUTOMATED BUILD - Qiita](https://qiita.com/takeshi_hirosue/items/9cfd22c2263866cece99 "Docker HubでDockerfileを公開する(AUTOMATED BUILD) - Qiita")
* [tomonorimatsumura/centos7-ansible - Docker Hub](https://hub.docker.com/r/tomonorimatsumura/centos7-ansible/ "tomonorimatsumura/centos7-ansible - Docker Hub")
* [tomonorimatsumura/ubuntu18-ansible - Docker Hub](https://hub.docker.com/r/tomonorimatsumura/ubuntu18-ansible/ "tomonorimatsumura/ubuntu18-ansible - Docker Hub")

##最後に

[CircleCI Advent Calendar 2018](https://qiita.com/advent-calendar/2018/circleci "CircleCI Advent Calendar 2018 - Qiita") の明日の記事は @ganta さんです