Ansibleによる開発をスケールさせるために、公式のベストプラクティスから逸脱した話



オンプレとAWSのハイブリット構成で、数千台の仮想マシンを `Ansible` で構成管理している環境で、実際に経験した問題と、有効だったリファクタリングについて説明します

少し前にTweetした↓の内容を、より詳細に書いています

<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">Ansibleのベストプラクティスでplaybook, inventory, vars, rolesを全部独立したディレクトリで管理してるけど、これスパゲッティにならずにメンテナンスできるのだろうか?<br><br>弊社では無理でinventoryごとにディレクトリ分割してplaybook, role, varsはinventoryにひもづけてます</p>&mdash; 入水岬@インフラエンジニア (@Mjusui) <a href="https://twitter.com/Mjusui/status/1178079993588408322?ref_src=twsrc%5Etfw">September 28, 2019</a></blockquote>


# 気がついたら、こんな事態に

社内でサービスの新機能開発プロジェクトが進行している中、インフラエンジニアとして、新機能用のサーバ構築をしていたときのことです


```
roles/ec2
roles/ec2/create
roles/ec2-create
```

EC2インスタンスを作成するための `roles` が乱立していました……

もしやと思いオンプレのコードを確認してみると

```
roles/create-vm
roles/virt-install
roles/v1.0/kvm/guest/install
```

やはり同じような `roles` が乱立していました

仮想マシンの作成だけでなく `apache2` `nginx` `mysql` などミドルウェアのインストールをするroleも、同じように別名で乱立している状態でした


# なぜこんなことに?

## AnsibleにおけるRolesとは

まずひとつは、そもそも `roles` という概念をとらえ損ねていることが原因でしょう

`roles` というのは言葉どおり**役割**ですから**「仮想マシン作成」**や**「Apache2をインストール」**のような処理の単位で分割するのではなく**「Webサーバ」**や**「DBサーバ」**のように、もっと大きな単位で分割すべきものです

公式ドキュメントの[例(ユーザガイド - Roles)](https://docs.ansible.com/ansible/latest/user_guide/playbooks_reuse_roles.html#role-directory-structure)でも、まさにそのように例示されています


`roles` を細かく分割すれば、それだけ汎用性、再利用性は高まりますが、中途半端な汎用性しか持ち合わせていないと **ほとんど同じ処理だけど、一部分だけ特殊なことをしたい** という場合に対応しきれなくなり、仕方なく新しく `roles` を作成する、ということになりがちです


## コードの依存関係

もうひとつは、メンテナンス性の問題です

1つ目とつながる部分もあるのですが、`roles` の汎用性、再利用性が高すぎると、無数のplaybookからロードされるようになるため、あるroleを変更した際 **どのplaybookに、あるいは、どのinventoryに影響するか分からない** といった状態になります

また、Ansibleのディレクトリ構成も、依存関係の把握を難しくしている要因の1つだと思います

下記はAnsibleの公式ドキュメントで、ディレクトリ構成のベストプラクティス([ユーザガイド - ディレクトリ構成](https://docs.ansible.com/ansible/latest/user_guide/playbooks_best_practices.html?highlight=best practice#directory-layout))として紹介されているものの1つを `inventory` `vars` `modules` `playbooks` `roles` という単位で分割して記載したものです(見やすさのため一部、省略)

```inventory.txt
production                # inventory file for production servers
staging                   # inventory file for staging environment
```

```vars.txt
group_vars/
   group1.yml             # here we assign variables to particular groups
   group2.yml
host_vars/
   hostname1.yml          # here we assign variables to particular systems
   hostname2.yml
```

```modules.txt
library/                  # if any custom modules, put them here (optional)
module_utils/             # if any custom module_utils to support modules, put them here (optional)
filter_plugins/           # if any custom filter plugins, put them here (optional)
```

```playbooks.txt
site.yml                  # master playbook
webservers.yml            # playbook for webserver tier
dbservers.yml             # playbook for dbserver tier
```

```roles.txt
roles/
    common/               # this hierarchy represents a "role"

~ commonの中身は省略 ~

    webtier/              # same kind of structure as "common" was above, done for the webtier role
    monitoring/           # ""
    fooapp/               # ""
```

この構成では `inventory` `vars` `playbooks` `roles` のディレクトリが全て同じ階層にあるので、どのplaybookから、どのroles,vars,inventoryが参照されているのか分かりづらくなっています

これらに加えてさらに `include_role` や `dependencies` でroleの中でroleを呼ぶような処理をしていると、コードに変更を加えた際の影響範囲は、いっそうは分かりづらくなっていきます

こうして、既存のroleのメンテナンスコストが増大し、結局あらたにroleを作成した方が速い、という流れになりがちなのです




# ディレクトリ構成を変えてみた

上記で紹介した問題は `roles` を、汎用的になりすぎないように分割することで、ある程度は解消できると思います

ただそれでも `roles - inventory - playbook` の依存関係が不明瞭なことには変わりありません

そこで以下のようにディレクトリ構成を変えてみました

```
regionA/　# オンプレ、AWSなど
  common/ # 一応commonも作る。ただし重複するコードでも、極力commonは利用しない
    roles/
    vars/
  serverA/
    inventory/
    roles/
    playbook1.yml (varsはディレクトリ化せず、playbookの中に直接書く)
    playbook2.yml (varsはディレクトリ化せず、playbookの中に直接書く)
    ...
  serverB/
    inventory/
    roles/
    playbook1.yml (varsはディレクトリ化せず、playbookの中に直接書く)
    playbook2.yml (varsはディレクトリ化せず、playbookの中に直接書く)
    ...
...

regionB/　# オンプレ、AWSなど
  common/ # 一応commonも作る。ただし重複するコードでも、極力commonは利用しない
  serverA/
    inventory/
    roles/
    playbook1.yml (varsはディレクトリ化せず、playbookの中に直接書く)
    playbook2.yml (varsはディレクトリ化せず、playbookの中に直接書く)
    ...
  serverB/
    inventory/
    roles/
    playbook1.yml (varsはディレクトリ化せず、playbookの中に直接書く)
    playbook2.yml (varsはディレクトリ化せず、playbookの中に直接書く)
    ...
...
```

## このディレクトリ構成の特徴

- `serverA` `serverB` というのが `Webサーバ` とか `DBサーバ` に対応する概念です。またAWSやオンプレなど、サーバが動作する基盤やネットワークからして異なるものは `region` としてディレクトリを分割しています
- `region` を分ける理由は `roles` の中でregion判定して条件分岐する(AWSの場合だけ実行するなど)といった記述を避けるためです
- 他にも、よくあるAnsibleの書き方として `CentOS` `Ubuntu` といったディストリビューションごとに分岐処理するようなコードがありますが、これも「`serverA` は同じディストリビューションであること」という制約を加えれば `serverA` ディレクトリの中では **ディストリビューションを調べるコードを書く必要がなくなります**
- よくよく考えるてみると **「Webサーバ」のroleは「Webサーバ以外」のinventoryやplaybookとひもづける必要はない** ということが分かってくるでしょう。なので、関連するroles, playbooks, inventoryは一つのディレクトリにまとめて「他のディレクトリとの依存関係は持たない」とした方が、コード改修時の影響範囲が明確でメンテナンス性が向上します
- この構成では、極力 `common` は利用しない方針にします  
例えば `serverA` と `serverB` の両方にApache2をインストールする、といった重複する処理があった場合にも `common` は利用せず `serverA` と　`serverB` の**それぞれのディレクトリに同じコードを配置します**  
こうしておけば将来的に `serverB` だけApache2のバージョンを上げたいとか、httpd.confを変更したいなど、サーバ固有のカスタマイズが必要になったときに `serverA` との **共通部分がないことによって、影響範囲が極小化され、コードの改修がスムーズに進むためです**
- それと `vars` はディレクトリ化せずにplaybookに直接書いた方が、コードの可読性が高まります(ただし`inventory_vars` は `inventory` に書いた方がよいです)

## 他にも嬉しい副作用

- 複数サーバ間で共有しているコードを極力排除することで、それぞれの構築担当者の書いたコードのconflictが減り、作業の並列性が向上します
- `serverA` `serverB` などのディレクトリ内では、構築担当者が比較的に自由なコーディングができるため、あらたに業務に参加する方の障壁が少なくてすみます
- たとえばAWSからAzureへ意向するような場合でも `region` を新しく追加するだけで済み、既存のコードとの依存関係を考える必要がないため、スムーズに進みます


# 最後に

Ansibleはコードのシンプルさを保つために、高度なプログラミング言語に実装されているような安全機能が実装されておらず、プログラマが注意してコード設計をする必要があります

ですが、それが面白いところでもあります